#include "buzzlock.h"
#include <errno.h>
#include <sys/types.h>
#include <sys/syscall.h>
#include <unistd.h>
#include <time.h>
#include <stdlib.h>
#include <stdio.h>
//Initialize the lock
void init_bzz(bzz_t *lockptr, int num_threads, useconds_t timeout) {
    *lockptr = malloc(sizeof(bzz));
    bzz_t lock = *lockptr;
    lock -> timeout = timeout;
    sem_init(&lock -> mutex, 0, num_threads);
    pthread_mutex_init(&lock -> queue_mutex, 0);
    pthread_cond_init(&lock -> black_cond, 0);
    pthread_cond_init(&lock -> old_gold_cond, 0);
    pthread_cond_init(&lock -> new_gold_cond, 0);
    lock -> tbl = create_table(1024);
    lock -> in_use = 0;
    lock -> black = 0;
    lock -> old_gold = 0;
    lock -> new_gold = 0;
    lock -> bsig = 0;
    lock -> ogsig = 0;
    lock -> ngsig = 0;
    lock -> num_threads = num_threads;
}

//Color the lock
void bzz_color(int color, bzz_t lock) {
    pthread_mutex_lock(&lock -> queue_mutex);
    tbl_insert(&lock -> tbl, syscall(SYS_gettid), color);
    pthread_mutex_unlock(&lock -> queue_mutex);
}

void bzz_lock(bzz_t lock) {
    //Grab the queue lock
    pthread_mutex_lock(&lock -> queue_mutex);
    //Get the color of the thread
    int color = tbl_get(lock -> tbl, syscall(SYS_gettid));
    DPRINT(("Trying to aquire lock with color %d\n", color));
    DPRINT(("Thread count\nBlack: %d\nNew Gold: %d\nOld Gold: %d\nIn use: %d\n", lock -> black, lock -> new_gold, lock -> old_gold, lock -> in_use));
    if(color == BZZ_BLACK) { //We have a black thread
        lock -> black++;
        //If we're the only thread in the queue, and the lock has a spot for us...
        if(lock -> in_use < lock -> num_threads && lock -> black == 1 && lock -> new_gold == 0 && lock -> old_gold == 0) {
            DPRINT(("Black given lock.\n"));
            //Grab the lock, update data
            sem_wait(&lock -> mutex);
            lock -> in_use++;
            lock -> black--;
            pthread_mutex_unlock(&lock -> queue_mutex);
            //DPRINT(("Black given lock.\n"));
        } else {
            DPRINT(("Black waiting\n"));
            //No spots, wait until we're signaled
            pthread_cond_wait(&lock -> black_cond, &lock -> queue_mutex);
            //Woke up, so aquire the lock
            sem_wait(&lock -> mutex);
            lock -> in_use++;
            lock -> black--;
            lock -> bsig--;
            //Release queue lock
            pthread_mutex_unlock(&lock -> queue_mutex);
            DPRINT(("Black aquired lock.\n"));
        }
    } else if(color == BZZ_GOLD) {
        //Create a timespec for how long the timeout is
        struct timespec time;
        struct timeval tv;
        gettimeofday(&tv, 0);
        time.tv_sec = tv.tv_sec;
        time.tv_nsec = tv.tv_usec + lock -> timeout;
        if (time.tv_nsec>=1000000)
        {
            time.tv_sec += time.tv_nsec / 1000000;
            time.tv_nsec %= 1000000;
        }
        time.tv_nsec *= 1000;
        lock -> new_gold++;
        //If we're the only thread in the queue, and the lock has a spot for us...
        if(lock -> in_use < lock -> num_threads && lock -> black == 0 && lock -> new_gold == 1 && lock -> old_gold == 0) {
            //Grab the lock, update data
            sem_wait(&lock -> mutex);
            lock -> in_use++;
            lock -> new_gold--;
            pthread_mutex_unlock(&lock -> queue_mutex);
            DPRINT(("New gold given lock.\n"));
        } else {
            //Wait, but wake up if you time out
            int res = pthread_cond_timedwait(&lock -> new_gold_cond, &lock -> queue_mutex, &time);
            if(res == ETIMEDOUT) { //If we timed out, make the new gold thread old
                DPRINT(("Res: %d\n", res));
                DPRINT(("Timed out, making new gold old.\n"));
                lock -> new_gold--;
                lock -> old_gold++;
                //If we're the only thread in the queue (pretty sure this is redundant)
                if(lock -> in_use < lock -> num_threads && lock -> black == 0 && lock -> new_gold == 0 && lock -> old_gold == 1) {
                    sem_wait(&lock -> mutex);
                    lock -> in_use++;
                    lock -> old_gold--;
                    pthread_mutex_unlock(&lock -> queue_mutex);
                    DPRINT(("Old gold given lock.\n"));
                } else {
                    //Wait to be signaled
                    pthread_cond_wait(&lock -> old_gold_cond, &lock -> queue_mutex);
                    //Woke up, so aquire the lock
                    sem_wait(&lock -> mutex);
                    lock -> in_use++;
                    lock -> old_gold--;
                    lock -> ogsig--;
                    //Release queue lock
                    pthread_mutex_unlock(&lock -> queue_mutex);
                    DPRINT(("Old gold aquired lock.\n"));
                }
            } else {
                //New gold thread was signaled, so give the lock
                DPRINT(("Res: %d\n", res));
                sem_wait(&lock -> mutex);
                lock -> in_use++;
                lock -> new_gold--;
                lock -> ngsig--;
                //Release queue lock
                pthread_mutex_unlock(&lock -> queue_mutex);
                DPRINT(("New gold aquired lock.\n"));
            }
        }
        
    } else {
        DPRINT(("Something weird happened!\n"));
    }
}

void bzz_release(bzz_t lock) {
    sem_post(&lock -> mutex);  //Inc our semaphore.  No one should be waiting on it though, so this just prepares it for the next step.
    
    //Grab the queue lock
    pthread_mutex_lock(&lock -> queue_mutex);
    DPRINT(("Color %d released lock\n", tbl_get(lock -> tbl, syscall(SYS_gettid))));
    DPRINT(("ogsig: %d, bsig: %d, ngsig: %d\n", lock -> ogsig, lock -> bsig, lock -> ngsig));
    lock -> in_use--;
    //Signal the correct waiting thread, in priority
    if(lock -> old_gold > lock -> ogsig) {
        DPRINT(("ogsig\n"));
        lock -> ogsig++;
        pthread_cond_signal(&lock -> old_gold_cond);
    } else if(lock -> black > lock -> bsig) {
        DPRINT(("bsig\n"));
        lock -> bsig++;
        pthread_cond_signal(&lock -> black_cond);
    } else if(lock -> new_gold > lock -> ngsig) {
        DPRINT(("ngsig\n"));
        lock -> ngsig++;
        pthread_cond_signal(&lock -> new_gold_cond);
    }
    pthread_mutex_unlock(&lock -> queue_mutex);
}

void bzz_kill(bzz_t lock) {
    //Free all malloc'd data
    free_table(lock -> tbl);
    sem_destroy(&lock -> mutex);
    pthread_mutex_destroy(&lock -> queue_mutex);
    pthread_cond_destroy(&lock -> black_cond);
    pthread_cond_destroy(&lock -> old_gold_cond);
    pthread_cond_destroy(&lock -> new_gold_cond);
    free(lock);
}

